package api

import (
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gogf/gf/container/gmap"
	"github.com/gorilla/websocket"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	ReqPagesAndFlv          int = 0
	ReqFlv                  int = 1
	ReqDownLoadVideo        int = 2
	ReqDownLoadList         int = 3
	ReqStopDownLoadVideo    int = 4
	ReqRestoreDownVideo     int = 5
	ReqStopAllDownLoadVideo int = 6
	ReqRestoreAllDownVideo  int = 7
)

type wsData struct {
	Type int         `json:"type"`
	Data interface{} `json:"data"`
}
type reqPagesData struct {
	Avid   string `json:"avid"`
	Cookie string `json:"cookie"`
}

//全局 ws 对象
var gws *websocket.Conn = nil

//下载列表
var downLoadMap *gmap.ListMap

//ws队列
var wsQueue chan gin.H

//发送ws消息
func wsSend(val gin.H) {
	wsQueue <- val
}

//消费ws消息
func consumption() {
	for v := range wsQueue {
		if gws != nil {
			err := gws.WriteJSON(v)
			if err != nil {
				fmt.Println("发送失败:", err)
			}
		}

	}
}

//上一次上传时间
var lastTime int64

//上报下载进度队列
var uploadQueue chan *reader

//发送下载进度
func uploadSend(r *reader) {
	uploadQueue <- r
}

//消费下载进度队列
func uploadConsumption() {
	for r := range uploadQueue {
		//1秒上传一次进度
		if time.Now().Unix()-lastTime > 1 || r.Current == r.Total {
			lastTime = time.Now().Unix()
			reqDownLoadListHandle()
		}
	}
}

//添加到下载列表
//k = md5V(avid+cid+fileName+"-"+order)
func addDownLoadMap(k string, v *downLoadInfo) {
	downLoadMap.Set(k, v)
}

//更新状态 -1 停止下载 1 正在下载 2下载完毕
func updateDownLoadState(k string, state int) {
	if v := downLoadMap.Get(k); v != nil {
		v.(*downLoadInfo).State = state
	}
}

//更新下载量
func updateDownLoadCurrent(k string, current int64) {
	if v := downLoadMap.Get(k); v != nil {
		v.(*downLoadInfo).Current = current
	}
}

//停止下载任务
func stopDownLoad(k string) {
	if v := downLoadMap.Get(k); v != nil {
		v.(*downLoadInfo).State = -1
		if e := v.(*downLoadInfo).Body.Close(); e != nil {
			fmt.Println(e.Error())
		}
	}

}

//移除下载任务
func removeDownLoad(k string) {
	downLoadMap.Remove(k)
}

//下载任务是否存在
//存在返回 true 并且返回下载信息
func isDownLoad(k string) (bool, *downLoadInfo) {
	if v := downLoadMap.Get(k); v != nil {
		return true, v.(*downLoadInfo)
	} else {
		return false, nil
	}

}

func init() {
	downLoadMap = gmap.NewListMap(true)
	wsQueue = make(chan gin.H, 1000)
	uploadQueue = make(chan *reader, 1000)
	go consumption()
	go uploadConsumption()
}

func WsHandle(ws *websocket.Conn) {
	gws = ws
	var wsData wsData
	for {
		err := gws.ReadJSON(&wsData)
		if err != nil {
			gws = nil
			break
		}
		switch wsData.Type {
		case ReqPagesAndFlv:
			{
				reqPagesHandle(&wsData)
			}
		case ReqFlv:
			{
				//reqFlvHandle(&wsData)
			}
		case ReqDownLoadVideo:
			{
				reqDownVideoHandle(&wsData, false, false)
			}
		case ReqDownLoadList:
			{
				reqDownLoadListHandle()
			}
		case ReqStopDownLoadVideo:
			{
				reqStopDownLoadVideoHandle(&wsData)
			}
		case ReqRestoreDownVideo:
			{
				reqRestoreDownVideoHandle(&wsData)
			}
		case ReqStopAllDownLoadVideo:
			{
				ReqStopAllDownLoadVideoHandle()
			}
		case ReqRestoreAllDownVideo:
			{
				ReqRestoreAllDownVideoHandle()
			}
		}
	}
}

//拉取标题丶目录结构丶下载源信息
func reqPagesHandle(wd *wsData) {
	var rpd reqPagesData
	iToS(&wd.Data, &rpd)
	if pageList, err := getPageList(rpd.Avid); err != nil {
		err := gws.WriteJSON(gin.H{
			"code": -1,
			"msg":  err,
		})
		if err != nil {
			fmt.Println(err)
		}
	} else {
		if len(pageList) > 0 {
			title, titleErr := getTile(rpd.Avid, strconv.Itoa(pageList[0].Cid))
			if titleErr != nil {
				title = "获取目录出错-" + rpd.Avid
			}
			//获取所有下载源
			allFlvList := make([][]flvDurl, len(pageList))
			var wg sync.WaitGroup
			for k, v := range pageList {
				wg.Add(1)
				go goGetFlvList(allFlvList, k, rpd.Avid, strconv.Itoa(v.Cid), &wg, rpd.Cookie)
			}
			wg.Wait()
			wsSend(gin.H{
				"code":     0,
				"type":     ReqPagesAndFlv,
				"title":    title,
				"pageList": pageList,
				"flvList":  allFlvList,
			})

		} else {
			wsSend(gin.H{
				"code": -1,
				"msg":  "啥都没有啊",
			})
		}

	}
}

//拉取下载列表
func reqDownLoadListHandle() {
	var downLoadList []*downLoadInfo
	downLoadMap.Iterator(func(k, v interface{}) bool {
		downLoadList = append(downLoadList, v.(*downLoadInfo))
		return true
	})
	wsSend(gin.H{
		"code": 0,
		"type": ReqDownLoadList,
		"data": downLoadList,
	})
}

type reqDownVideoData struct {
	Urls     []flvDurl `json:"urls"`
	DirName  string    `json:"dirName"`
	FileName string    `json:"fileName"`
	Avid     int       `json:"avid"`
	Cid      int       `json:"cid"`
}

//批量请求下载视频
// i 参数是判断是不是恢复下载的 因为恢复下载传递的参数不需要拼接 Order
// all 参数判断是否全部恢复 是的话就不推送消息不然卡死
func reqDownVideoHandle(wd *wsData, i bool, all bool) {
	var rads []reqDownVideoData
	iToS(&wd.Data, &rads)
	for _, rad := range rads {
		for _, u := range rad.Urls {
			var md5v string
			if i {
				md5v = md5V(strconv.Itoa(rad.Avid) + strconv.Itoa(rad.Cid) + rad.FileName)
			} else {
				md5v = md5V(strconv.Itoa(rad.Avid) + strconv.Itoa(rad.Cid) + rad.FileName + "-" + strconv.Itoa(u.Order))
			}
			b, v := isDownLoad(md5v)
			//正在下载或者已经下载完成
			if b && (v.State == 1 || v.State == 2) {
				msg := ""
				if i {
					msg = rad.FileName + "已经在下载列表了"
				} else {
					msg = rad.FileName + "-" + strconv.Itoa(u.Order) + "已经在下载列表了"
				}
				wsSend(gin.H{
					"code": -1,
					"type": ReqDownLoadVideo,
					"msg":  msg,
				})
				continue
			}
			//如果存在并且是暂停的
			if b && v.State == -1 {
				stopDownLoad(md5v)
				updateDownLoadState(md5v, 1)
			}
			if i {
				go goDownVideo(u.Url, rad.DirName, rad.FileName, u.Size, rad.Avid, rad.Cid, u.Order)
				if !all {
					wsSend(gin.H{
						"code": 0,
						"type": ReqRestoreDownVideo,
						"data": rad,
					})
				}
			} else {
				go goDownVideo(u.Url, rad.DirName, rad.FileName+"-"+strconv.Itoa(u.Order), u.Size, rad.Avid, rad.Cid, u.Order)
			}
		}

	}

}

//批量请求恢复下载
func reqRestoreDownVideoHandle(wd *wsData) {
	reqDownVideoHandle(wd, true, false)
}

type reqStopDownLoadVideoData struct {
	FileName string `json:"fileName"`
	Avid     int    `json:"avid"`
	Cid      int    `json:"cid"`
	Order    int    `json:"order"`
}

//批量请求停止下载任务
func reqStopDownLoadVideoHandle(wd *wsData) {
	var revs []reqStopDownLoadVideoData
	iToS(&wd.Data, &revs)
	for _, rev := range revs {
		md5V := md5V(strconv.Itoa(rev.Avid) + strconv.Itoa(rev.Cid) + rev.FileName)
		if b, info := isDownLoad(md5V); b && info.State == 1 {
			stopDownLoad(md5V)
		}
		wsSend(gin.H{
			"code": 0,
			"type": ReqStopDownLoadVideo,
			"data": rev,
		})
	}
}

//暂停所有下载
func ReqStopAllDownLoadVideoHandle() {
	downLoadMap.Iterator(func(k, v interface{}) bool {
		if v.(*downLoadInfo).State == 1 {
			stopDownLoad(k.(string))
		}

		return true
	})
	wsSend(gin.H{
		"code": 0,
		"type": ReqStopAllDownLoadVideo,
	})
}

//恢复所有下载
func ReqRestoreAllDownVideoHandle() {
	var rads []*reqDownVideoData
	downLoadMap.Iterator(func(k, v interface{}) bool {
		d := v.(*downLoadInfo)
		if d.State == -1 {
			var urls []flvDurl
			urls = append(urls, flvDurl{
				Size:  d.Url.Size,
				Url:   d.Url.Url,
				Order: d.Url.Order,
			})
			rads = append(rads, &reqDownVideoData{
				Urls:     urls,
				DirName:  d.DirName,
				FileName: d.FileName,
				Avid:     d.Avid,
				Cid:      d.Cid,
			})
		}

		return true
	})
	wd := &wsData{
		Type: ReqRestoreAllDownVideo,
		Data: rads,
	}
	reqDownVideoHandle(wd, true, true)
	wsSend(gin.H{
		"code": 0,
		"type": ReqRestoreAllDownVideo,
	})
}

type recv struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}
type pageListRecv struct {
	recv
	Data []pageData `json:"data"`
}
type pageData struct {
	Cid  int    `json:"cid"`
	Page int    `json:"page"`
	Part string `json:"part"`
}

func getPageList(aid string) (pageList []pageData, err error) {
	res, err := http.Get("https://api.bilibili.com/x/player/pagelist?aid=" + aid + "&jsonp=jsonp")
	if err != nil {
		return nil, err
	}
	defer func() { _ = res.Body.Close() }()
	var plr pageListRecv
	if err := json.NewDecoder(res.Body).Decode(&plr); err != nil {
		return nil, err
	}
	return plr.Data, nil
}

type titleRecv struct {
	recv
	Data titleData `json:"data"`
}
type titleData struct {
	Title string `json:"title"`
}

func getTile(aid string, cid string) (title string, err error) {
	res, err := http.Get("https://api.bilibili.com/x/web-interface/view?aid=" + aid + "&cid=" + cid)
	if err != nil {
		return "", err
	}
	defer func() { _ = res.Body.Close() }()
	var tr titleRecv
	if err := json.NewDecoder(res.Body).Decode(&tr); err != nil {
		return "", err
	}
	return tr.Data.Title, nil
}

type flvRecv struct {
	recv
	Data flvData `json:"data"`
}
type flvData struct {
	Durl []flvDurl `json:"durl"`
}
type flvDurl struct {
	Length int64  `json:"length"`
	Size   int64  `json:"size"`
	Url    string `json:"url"`
	Order  int    `json:"order"`
}

func getFlvList(avid string, cid string, cookie string) (flvList []flvDurl, err error) {
	client := &http.Client{}

	req, _ := http.NewRequest("GET", "https://api.bilibili.com/x/player/playurl?avid="+avid+"&cid="+cid+"&qn=80&type=&otype=json", nil)
	if req == nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo  http.NewRequest req返回nil",
		})
		return
	}
	req.Header.Add("Cookie", cookie)
	res, err := client.Do(req)
	if err != nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  err,
		})
		return
	}
	defer func() { _ = res.Body.Close() }()
	var fr flvRecv
	if err := json.NewDecoder(res.Body).Decode(&fr); err != nil {
		return nil, err
	}
	return fr.Data.Durl, nil
}

func goGetFlvList(allFlvList [][]flvDurl, index int, avid string, cid string, wg *sync.WaitGroup, cookie string) {
	defer wg.Add(-1)
	flvList, _ := getFlvList(avid, cid, cookie)
	allFlvList[index] = flvList
}

type reader struct {
	io.Reader
	Total    int64
	Current  int64
	Avid     int
	Cid      int
	Order    int
	FileName string
}
type downLoadInfo struct {
	Body     io.ReadCloser
	DirName  string   `json:"dirName"`
	FileName string   `json:"fileName"`
	Url      *flvDurl `json:"url"`
	Avid     int      `json:"avid"`
	Order    int      `json:"order"`
	Cid      int      `json:"cid"`
	Total    int64    `json:"total"`
	Current  int64    `json:"current"`
	State    int      `json:"state"`
}

func goUpdateDownLoadCurrent(avid int, cid int, fileName string, current int64) {
	updateDownLoadCurrent(md5V(strconv.Itoa(avid)+strconv.Itoa(cid)+fileName), current)
}

func (r *reader) Read(p []byte) (n int, err error) {
	n, err = r.Reader.Read(p)
	if err != nil && err != io.EOF {
		//修改下载任务状态为 停止
		updateDownLoadState(md5V(strconv.Itoa(r.Avid)+strconv.Itoa(r.Cid)+r.FileName), -1)
		if strings.Contains(err.Error(), "use of closed network connection") ||
			strings.Contains(err.Error(), "read on closed response body") || err == io.ErrUnexpectedEOF {
			//fmt.Println("````````````````" + err.Error())
			return
		}
		//fmt.Println("Read:下载函数出错", err)
		return
	}
	r.Current += int64(n)
	go goUpdateDownLoadCurrent(r.Avid, r.Cid, r.FileName, r.Current)
	uploadSend(r)
	return
}
func goDownVideo(url string, dirName string, fileName string, fileTotal int64, avid int, cid int, order int) {
	//先添加一个空的
	//addDownLoadMap(md5V(strconv.Itoa(avid)+strconv.Itoa(cid)+fileName), nil)
	//去掉 ? 号 不然创建目录失败不知道为啥？？
	dirName = strings.Replace(dirName, "?", "", -1)
	fileName = strings.Replace(fileName, "?", "", -1)
	client := &http.Client{}
	req, _ := http.NewRequest("GET", url, nil)
	if req == nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo  http.NewRequest req返回nil",
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	mkdirErr := os.MkdirAll("./video/"+dirName+"/"+fileName+"/", os.ModePerm)
	if mkdirErr != nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo 目录创建失败 os.MkdirAll" + mkdirErr.Error(),
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	//先判断是否已经下好
	if exists, pathErr := PathExists("./video/" + dirName + "/" + fileName + "/" + fileName + ".flv"); pathErr != nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo 判断视频是否已经下好出错" + pathErr.Error(),
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	} else {
		//如果已经下好
		if exists {
			wsSend(gin.H{
				"code": -1,
				"type": ReqDownLoadVideo,
				"msg":  "视频已存在不能重复下载",
			})
			//移除空下载任务
			//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
			return
		}
	}

	filePath := "./video/" + dirName + "/" + fileName + "/" + fileName
	//判断文件是否存在
	exists, pathErr := PathExists(filePath + ".download")
	if pathErr != nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo 判断文件是否存在出错" + pathErr.Error(),
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	Range := "bytes=0-"
	var fileSize int64 = 0
	//如果文件存在
	if exists {
		fileSize = getFileSize(filePath + ".download")
		//如果视频已经下载完成但是没来得及改后辍
		if fileSize >= fileTotal {
			if e := os.Rename(filePath+".download", filePath+".flv"); e != nil {
				wsSend(gin.H{
					"code": -1,
					"type": ReqDownLoadVideo,
					"msg":  "goDownVideo 文件重命名出错" + e.Error(),
				})
			}
			//移除空下载任务
			//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
			return
		}
		Range = "bytes=" + strconv.FormatInt(fileSize, 10) + "-"
	}
	req.Header.Add("Accept", "*/*")
	req.Header.Add("Accept-Encoding", "identity")
	req.Header.Add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
	req.Header.Add("Connection", "keep-alive")
	req.Header.Add("Host", GetBetweenStr(url, "http://", "/"))
	req.Header.Add("Origin", "https://www.bilibili.com")
	req.Header.Add("Referer", "https://www.bilibili.com/video/av"+strconv.Itoa(avid))
	req.Header.Add("Range", Range)
	res, err := client.Do(req)
	if err != nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  err,
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	defer func() { _ = res.Body.Close() }()
	if res == nil {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg": "goDownVideo  client.Do	 req返回nil",
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	file, er := os.OpenFile(filePath+".download", os.O_WRONLY|os.O_APPEND, os.ModePerm)
	defer func() { _ = file.Close() }()
	if er != nil && os.IsNotExist(er) {
		file, er = os.Create(filePath + ".download")
		if er != nil {
			wsSend(gin.H{
				"code": -1,
				"type": ReqDownLoadVideo,
				"msg":  "goDownVideo  os.Create 错误:" + er.Error(),
			})
			//移除空下载任务
			//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
			return
		}
	}
	if res.StatusCode != 206 {
		wsSend(gin.H{
			"code": -1,
			"type": ReqDownLoadVideo,
			"msg":  "goDownVideo StatusCode 不是206",
		})
		//移除空下载任务
		//removeDownLoad(md5V(strconv.Itoa(avid) + strconv.Itoa(cid) + fileName))
		return
	}
	r := &reader{
		Reader:   res.Body,
		Total:    fileTotal,
		Current:  fileSize,
		Avid:     avid,
		Cid:      cid,
		Order:    order,
		FileName: fileName,
	}
	dli := &downLoadInfo{
		Body:     res.Body,
		DirName:  dirName,
		FileName: fileName,
		Url: &flvDurl{
			Length: 0,
			Size:   fileTotal,
			Url:    url,
			Order:  order,
		},
		Avid:    avid,
		Cid:     cid,
		Order:   order,
		Total:   fileTotal,
		Current: fileSize,
		State:   1,
	}
	//添加到下载列表
	addDownLoadMap(md5V(strconv.Itoa(avid)+strconv.Itoa(cid)+fileName), dli)
	if _, pathErr := io.Copy(file, r); pathErr != nil {
		if strings.Contains(pathErr.Error(), "read on closed response body") ||
			strings.Contains(pathErr.Error(), "use of closed network connection") {
			//fmt.Println("````````````````222" + pathErr.Error())
			return
		}
		//重启下载
		if pathErr == io.ErrUnexpectedEOF || strings.Contains(pathErr.Error(), "wsarecv") {
			go goDownVideo(url, dirName, fileName, fileTotal, avid, cid, order)
			fmt.Println("重启下载")
			return
		}
		//修改下载状态为停止
		updateDownLoadState(md5V(strconv.Itoa(r.Avid)+strconv.Itoa(r.Cid)+r.FileName), -1)
		fmt.Println("io.Copy出错:" + pathErr.Error())
		return
	}
	//文件重命名
	_ = file.Close()
	fileSize = getFileSize(filePath + ".download")
	if fileSize >= fileTotal {
		if e := os.Rename(filePath+".download", filePath+".flv"); e != nil {
			wsSend(gin.H{
				"code": -1,
				"type": ReqDownLoadVideo,
				"msg":  "goDownVideo 文件重命名出错" + e.Error(),
			})
		}
		//2表示下载完毕
		updateDownLoadState(md5V(strconv.Itoa(avid)+strconv.Itoa(cid)+fileName), 2)
		return
	}
}

//获取文本中间内容
func GetBetweenStr(str, start, end string) string {
	n := strings.Index(str, start)
	if n == -1 {
		n = 0
	} else {
		n = n + len(start)
	}
	str = string([]byte(str)[n:])
	m := strings.Index(str, end)
	if m == -1 {
		m = len(str)
	}
	str = string([]byte(str)[:m])
	return str
}

//判断文件是否存在
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

//获取文件大小
func getFileSize(filename string) int64 {
	var result int64
	_ = filepath.Walk(filename, func(path string, f os.FileInfo, err error) error {
		result = f.Size()
		return nil
	})
	return result
}

//从src interface{} 数据 写入 dst 结构体,& 引用传递
func iToS(src interface{}, dst interface{}) {
	marshal, _ := json.Marshal(src)
	_ = json.Unmarshal(marshal, dst)
}

//md5加密
func md5V(str string) string {
	h := md5.New()
	h.Write([]byte(str))
	return hex.EncodeToString(h.Sum(nil))
}
